#!/usr/bin/env python
"""
Parser for NorduGrid's arc.conf file.
"""
# last change: removed with_statement impl. due to backwards compatibility

__author__ = "Placi Flury grid@switch.ch"
__copyright__ = "Copyright 2010, SMSCG an AAA/SWITCH project"
__date__ = "11.11.2010"
__version__ = "0.1.2"
__all__ = ['ParseError', 'SanityError', 'ARC_Parser']

import os.path

class ParseError(Exception):
    """ 
    Exception raised for Parser errors.
    Attributes:
        expression -- input expression in which error occurred
        message -- explanation of error 
    """
    def __init__(self, expression, message):
        Exception.__init__(self)
        self.expression = expression
        self.message = message

class SanityError(Exception):
    """ 
    Exception raised for Sanity check  errors.
    Attributes:
        expression -- input expression in which error occurred
        message -- explanation of error 
    """
    def __init__(self, expression, message):
        Exception.__init__(self)
        self.expression = expression
        self.message = message

class NoComments_file(file):
    """ Remove comment lines while reading file. """    
    def __init__(self, name, mode, comment_tag):
        file.__init__(self, name, mode)
        self.comment_tag = comment_tag

    def next(self):
        while True:
            line = file.next(self).strip()
            if len(line) <= 1: 
                continue
            if line.startswith(self.comment_tag):
                continue
            return line


class ARC_Parser(object):
    """
    Parser for Nordugrid ARCS configuration (conventionally 
    arc.conf). Raises ParseError or Sanity exception if 
    things go wrong. 
    
    Notice: comments are only allowd at beginning of line. 
    """  

    valid_block_names = ['common', 'grid-manager', 'gridftpd',
            'httpsd', 'group', 'vo', 'infosys', 
            'registration', 'cluster', 'queue', 'se', 'rc']

    multi_value_keys = ['unixgroup', 'unixmap']
    no_value_keys = ['all']   # e.g. 'all' in [group] blocks 

    comment_tag = '#'
 
    def __init__(self, arcfile):
        self.config_file = arcfile
        self.map = dict()
        self.group_pooldir = dict()
        self.group_vo = dict()

        if os.path.exists(arcfile) and os.path.isfile(arcfile):
            self.read_file(arcfile)
        else:
            raise ParseError("No/Not a file", 
                "'%s' doesn't exists or is not a file" % arcfile) 

    def read_file(self, arcfile):
        try:
            f = NoComments_file(arcfile, 'r', ARC_Parser.comment_tag) 
            in_block = None
            for l in f:
                if l.startswith('['):
                    full_name = l.lstrip(' [').rstrip(' ]')
                    block_type = None
                    block_name = None
                    next = 0
                    if '/' in full_name:
                        sp = full_name.split('/')
                        block_type = sp[0]
                        if len(sp) == 2: # e.g. [group/smscg] 
                            block_name = sp[1]
                    else:
                        block_type = full_name
                    if not self.map.has_key(block_type):
                        if not block_name: 
                            block_name = block_type
                        self.map[block_type] = {'_count': next,  next : dict(name=block_name)}
                    else: 
                        next = self.map[block_type]['_count'] + 1
                        self.map[block_type]['_count'] = next
                        self.map[block_type][next] =  dict(name = block_name)
                    in_block = self.map[block_type][next]
                else: # within block
                    # remove potential comment within line
                    li  = l.split(ARC_Parser.comment_tag)[0].strip()                    
                    if li in ARC_Parser.no_value_keys:
                        kv = li
                        value= 'no_value_key'
                        assignment = '%s="%s"' % (kv,value)
                        exec(assignment)
                    else:
                        try: # bash-like key-value assignments. 
                            kv = li.split('=')
                            key = kv[0].strip()
                            value = kv[1].strip().strip('\'\"')
                            assignment = '%s="%s"' % (key,value)
                            exec(assignment)
                        except:
                            raise ParseError('Invalid assignment', "Assignmnet '%s' in [%s]" % (li,full_name))
                    
                    if key in ARC_Parser.multi_value_keys:
                        if not in_block.has_key(key) :
                            in_block[key]=list()
                        in_block[key].append(eval(key))
                    else:
                        in_block[key] = eval(key) 
        finally:
            f.close()

        self.map_size = len(self.map)
        self.group_pooldir = dict()
        self.group_vo = dict()

 
    def print_dict(self, d, space=0):
        for  key, value in d.items():
            if key == '_count': continue
            print ' ' * space,  key, '--', 
            if type(value) == dict:
                nspace = space + 5
                self.print_dict(value, space= nspace)
            else:
                print value
   
    def generator_get_block(self, block_type):
        """ Each call it returns next [block_type] or [block_type/*] 
            block of arc.conf (or none, if all have been returned)"""

        
        if not self.map.has_key(block_type):
            return 
        
        num_items = self.map[block_type]['_count']
        i = -1
        while i < num_items:
            i += 1
            yield self.map[block_type][i]
            
            
    def get_gridftpd_block(self):
        """ returns dict() of  *first* gridftpd block, with
            key-values of settings.
            Raises ParseError if no gridftp block is found.
        """
        try:
            return self.generator_get_block('gridftpd').next()
        except StopIteration:
            raise ParseError('[gridftpd] block missing', 
                    "[gridftpd] main block missing in '%s' config." % self.config_file)
        
    
    def set_group2pooldir_map(self):
        """ returns dict of groups and their corresponding
            pool-account directory. 
        """
        gridftpd = self.get_gridftpd_block() 
        if gridftpd.has_key('unixgroup'):
            for g in gridftpd['unixgroup']:
                if 'simplepool' in g:
                    group, s, pool_dir =  g.split()
                    self.group_pooldir[group] = pool_dir

    def get_group2pooldir_map(self):
        """ returns  group to pool directory  mappings """
        return self.group_pooldir

    def set_group2vo_map(self):
        """ sets  group to vo mappings """
        for group in self.generator_get_block('group'):
            if not group.has_key('name'):
                continue
            if not group.has_key('voms'):
                continue

            name = group['name']
            voms = group['voms']
            vo_name = voms.split()[0]
            self.group_vo[name] = vo_name

    def get_group2vo_map(self):
        """ return group to vo mappings """
        return self.group_vo

   
    def sanity_check(self):
        """
        Checks whether:
            - each enabled group simplepool has an 
              associated [group/*] or [group] block
                
        
        raises SanityError 
        """
        for group_name in self.group_pooldir.keys():
            if group_name not in self.group_vo.keys():
                raise SanityError("Group has no associated  group-block",
                "unixgroup '%s' in [gridftpd] has no associated [group/*] or [group] block." %
                (group_name))


    def main(self):
        """ main method """
        self.set_group2pooldir_map()
        self.set_group2vo_map()
        self.sanity_check()

if __name__ == '__main__':
        
    import sys
    if len(sys.argv) == 2: 
        afile = sys.argv[1]
    else:
        afile = '/etc/arc.conf'
    
    try:
        parser = ARC_Parser(afile)
        parser.main()
    except ParseError, e1:
        print "Could not parse file. Got '%s'" % e1.message
    except SanityError, e2:
        print "Sanity check failed with: '%s'" % e2.message
    
    print parser.get_group2vo_map()
    print parser.get_group2pooldir_map()




